﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AmiEmuLib.AmigaMachine.AmigaMemory;
using System.Diagnostics;
using System.Xml;
using System.Collections;
using TomanuExtensions;

namespace AmiEmuLib.SaveState
{
    /// <summary>
    /// Bank memory representation, whole or diff.
    /// </summary>
    [DebuggerDisplay("{BankType}, Address: {Address}, Count = {Count}, Prev = {Prev != null}")]
    public class MemoryBankState
    {
        /// <summary>
        /// Bank memory representation, whole or diff.
        /// </summary>
        private List<MemoryRange> m_ranges = new List<MemoryRange>();

        /// <summary>
        /// Previous memory bank.
        /// </summary>
        public MemoryBankState Prev { get; internal set; }

        /// <summary>
        /// Bank start address.
        /// </summary>
        [DebuggerDisplay("{Hex.UIntToHex(Address)}")]
        public uint Address { get; private set; }

        /// <summary>
        /// Bank type.
        /// </summary>
        public MemoryBankType BankType { get; private set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        internal MemoryBankState(XmlReader a_reader)
        {
            BankType = a_reader.GetAttributeEnum<MemoryBankType>("BankType");
            Address = a_reader.GetAttributeHexUInt("Address");

            a_reader.ReadStartElement(GetType().Name);

            while (a_reader.IsStartElement())
                m_ranges.Add(MemoryRange.Create(a_reader, this));

            a_reader.ReadEndElement();
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="a_bank"></param>
        /// <param name="a_prev"></param>
        internal MemoryBankState(MemoryBank a_bank, MemoryBankState a_prev = null)
        {
            Address = a_bank.Address;
            BankType = a_bank.Type;

            Prev = a_prev;

            if (a_prev == null)
                m_ranges.Add(new AreaMemoryRange(this, Address, Consts.BANK_SIZE));
            else
            {
                bool begin = false;
                uint begin_addr = 0;

                for (uint addr = Address; addr <= LastWordAddress; addr += 2)
                {
                    if (!begin)
                    {
                        if (a_bank.GetWord(addr) == a_prev.GetWord(addr))
                            continue;
                        begin = true;
                        begin_addr = addr;
                    }
                    else
                    {
                        if (a_bank.GetWord(addr) != a_prev.GetWord(addr))
                            continue;

                        begin = false;

                        m_ranges.Add(new AreaMemoryRange(this, begin_addr, addr - begin_addr));
                    }
                }

                if (begin)
                    m_ranges.Add(new AreaMemoryRange(this, begin_addr, Address + Consts.BANK_SIZE - begin_addr));
            }

            CreateFillRanges(a_bank);
            MergeRanges(); 
            Persist(a_bank);
        }

        /// <summary>
        /// Merge adhere ranges to decrease xml size and complexity.
        /// </summary>
        private void MergeRanges()
        {
            for (; ; )
            {
                int old = m_ranges.Count();

                for (int i = 0; i < m_ranges.Count() - 2; i++)
                {
                    MemoryRange mr1 = m_ranges[i];
                    MemoryRange mr2 = m_ranges[i + 1];

                    if (mr1.Address + mr1.Size != mr2.Address)
                        continue;

                    uint size = mr1.Size + mr2.Size;

                    if (mr1.GetCharSize() + mr2.GetCharSize() <
                        AreaMemoryRange.GetCharSize(size))
                    {
                        continue;
                    }

                    m_ranges.RemoveAt(i);
                    m_ranges.RemoveAt(i);

                    AreaMemoryRange mr = new AreaMemoryRange(mr1.BankState, mr1.Address, size);

                    m_ranges.Insert(i, mr);
                }

                if (old == m_ranges.Count)
                    break;
            }
        }

        /// <summary>
        /// Find and create fill ranges.
        /// </summary>
        private void CreateFillRanges(MemoryBank a_bank)
        {
            for (int i = 0; i < m_ranges.Count; i++)
            {
                if (!(m_ranges[i] is AreaMemoryRange))
                    continue;

                AreaMemoryRange mr = m_ranges[i] as AreaMemoryRange;

                uint start = mr.Address;
                uint end = mr.Address + 2;
                ushort pattern = a_bank.GetWord(start);

                while (end <= mr.LastWordAddress + 2)
                {
                    ushort new_pattern = pattern;
                    if (end <= mr.LastWordAddress)
                        new_pattern = a_bank.GetWord(end);

                    if ((new_pattern != pattern) || (end == mr.LastWordAddress + 2))
                    {
                        if (end - start > 2)
                        {
                            uint size1 = start - mr.Address;
                            uint address1 = mr.Address;

                            uint size2 = end - start;
                            uint address2 = start;

                            uint size3 = mr.Address + mr.Size - end;
                            uint address3 = end;

                            if (AreaMemoryRange.GetCharSize(size1) + 
                                FillMemoryRange.GetCharSize(size2) +
                                AreaMemoryRange.GetCharSize(size3) < mr.GetCharSize())
                            {
                                m_ranges.RemoveAt(i);

                                if (size3 > 0)
                                {
                                    m_ranges.Insert(i, new AreaMemoryRange(mr.BankState,
                                        address3, size3));
                                }

                                m_ranges.Insert(i, new FillMemoryRange(mr.BankState, 
                                    address2, size2));

                                if (size1 > 0)
                                {
                                    m_ranges.Insert(i, new AreaMemoryRange(mr.BankState,
                                        address1, size1));
                                }

                                break;
                            }
                        }

                        start = end;
                        pattern = new_pattern;
                    }

                    end += 2;
                } 
            }
        }

        /// <summary>
        /// Get range last addres sword.
        /// </summary>
        public uint LastWordAddress
        {
            get
            {
                return Address + Consts.BANK_SIZE - 2;
            }
        }

        /// <summary>
        /// Check if address is in range.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        public bool InRange(uint a_address)
        {
            if (!MemoryHelpers.IsWordAllign(a_address))
                throw new ArgumentException();

            return (a_address >= Address) && ((a_address - Address) < Consts.BANK_SIZE);
        }

        /// <summary>
        /// Get word from address. Valid only after persist or load.
        /// </summary>
        /// <param name="a_address"></param>
        /// <returns></returns>
        public ushort GetWord(uint a_address)
        {
            foreach (var mr in m_ranges)
            {
                if (mr.InRange(a_address))
                    return mr.GetWord(a_address);
            }

            return Prev.GetWord(a_address);
        }

        /// <summary>
        /// Write xml.
        /// </summary>
        /// <param name="a_writer"></param>
        internal void WriteXml(XmlWriter a_writer)
        {
            a_writer.WriteAttribute("BankType", BankType);
            a_writer.WriteHexAttribute("Address", Address);

            foreach (var mr in m_ranges)
            {
                if (mr is AreaMemoryRange)
                    a_writer.WriteStartElement("Area");
                else if (mr is FillMemoryRange)
                    a_writer.WriteStartElement("Fill");
                else
                    throw new NotImplementedException();
                    
                mr.WriteXml(a_writer);

                a_writer.WriteEndElement();
            }
        }

        /// <summary>
        /// Restore memory state.
        /// </summary>
        internal void Restore(Memory a_state)
        {
            foreach (var mr in m_ranges)
                mr.Restore(a_state);
        }

        /// <summary>
        /// Persists bank state from memory. Used at the end of bank state construction after 
        /// range optimizations.
        /// </summary>
        /// <param name="a_state"></param>
        internal void Persist(MemoryBank a_bank)
        {
            foreach (var mr in m_ranges)
                mr.Persist(a_bank);
        }

        public override bool Equals(object a_obj)
        {
            if (a_obj == null)
                return false;
            MemoryBankState bank_state = a_obj as MemoryBankState;
            if (bank_state == null)
                return false;

            return (Address == bank_state.Address) &&
                   m_ranges.SequenceEqual(bank_state.m_ranges);
        }

        public override int GetHashCode()
        {
            int hash = 0;

            foreach (var mem_range in m_ranges)
                hash ^= mem_range.GetHashCode();

            return hash;
        }

        /// <summary>
        /// Bank memory representation, whole or diff.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<MemoryRange> Ranges
        {
            get
            {
                return m_ranges;
            }
        }

        /// <summary>
        /// Get memory range at index.
        /// </summary>
        /// <param name="a_index"></param>
        /// <returns></returns>
        public MemoryRange this[int a_index]
        {
            get
            {
                return m_ranges[a_index];
            }
        }
    }
}
